globSync.GlobSync = GlobSync;
import { realpath as rp } from "fs";
import minimatch from "minimatch";
import path, { isAbsolute } from "path";
import assert from "assert";
import { setopts, ownProp, childrenIgnored, isIgnored } from "./common";

function globSync(pattern, options) {
    if (typeof options === "function" || arguments.length === 3)
        throw new TypeError(
            "callback provided to sync glob\n" +
                "See: https://github.com/isaacs/node-glob/issues/167"
        );

    return new GlobSync(pattern, options).found;
}

function GlobSync(pattern, options) {
    if (!pattern) throw new Error("must provide pattern");

    if (typeof options === "function" || arguments.length === 3)
        throw new TypeError(
            "callback provided to sync glob\n" +
                "See: https://github.com/isaacs/node-glob/issues/167"
        );

    if (!(this instanceof GlobSync)) return new GlobSync(pattern, options);

    setopts(this, pattern, options);

    if (this.noprocess) return this;

    var n = this.minimatch.set.length;
    this.matches = new Array(n);
    for (var i = 0; i < n; i++) {
        this._process(this.minimatch.set[i], i, false);
    }
    this._finish();
}

GlobSync.prototype._finish = function () {
    assert(this instanceof GlobSync);
    if (this.realpath) {
        var self = this;
        this.matches.forEach(function (matchset, index) {
            var set = (self.matches[index] = Object.create(null));
            for (var p in matchset) {
                try {
                    p = self._makeAbs(p);
                    var real = rp.realpathSync(p, self.realpathCache);
                    set[real] = true;
                } catch (er) {
                    if (er.syscall === "stat") set[self._makeAbs(p)] = true;
                    else throw er;
                }
            }
        });
    }
    common.finish(this);
};

GlobSync.prototype._process = function (pattern, index, inGlobStar) {
    assert(this instanceof GlobSync);

    // Get the first [n] parts of pattern that are all strings.
    var n = 0;
    while (typeof pattern[n] === "string") {
        n++;
    }
    // now n is the index of the first one that is *not* a string.

    // See if there's anything else
    var prefix;
    switch (n) {
        // if not, then this is rather simple
        case pattern.length:
            this._processSimple(pattern.join("/"), index);
            return;

        case 0:
            // pattern *starts* with some non-trivial item.
            // going to readdir(cwd), but not include the prefix in matches.
            prefix = null;
            break;

        default:
            // pattern has some string bits in the front.
            // whatever it starts with, whether that's 'absolute' like /foo/bar,
            // or 'relative' like '../baz'
            prefix = pattern.slice(0, n).join("/");
            break;
    }

    var remain = pattern.slice(n);

    // get the list of entries.
    var read;
    if (prefix === null) read = ".";
    else if (
        isAbsolute(prefix) ||
        isAbsolute(
            pattern
                .map(function (p) {
                    return typeof p === "string" ? p : "[*]";
                })
                .join("/")
        )
    ) {
        if (!prefix || !isAbsolute(prefix)) prefix = "/" + prefix;
        read = prefix;
    } else read = prefix;

    var abs = this._makeAbs(read);

    //if ignored, skip processing
    if (childrenIgnored(this, read)) return;

    var isGlobStar = remain[0] === minimatch.GLOBSTAR;
    if (isGlobStar)
        this._processGlobStar(prefix, read, abs, remain, index, inGlobStar);
    else this._processReaddir(prefix, read, abs, remain, index, inGlobStar);
};

GlobSync.prototype._processReaddir = function (
    prefix,
    read,
    abs,
    remain,
    index,
    inGlobStar
) {
    var entries = this._readdir(abs, inGlobStar);

    // if the abs isn't a dir, then nothing can match!
    if (!entries) return;

    // It will only match dot entries if it starts with a dot, or if
    // dot is set.  Stuff like @(.foo|.bar) isn't allowed.
    var pn = remain[0];
    var negate = !!this.minimatch.negate;
    var rawGlob = pn._glob;
    var dotOk = this.dot || rawGlob.charAt(0) === ".";

    var matchedEntries = [];
    for (var i = 0; i < entries.length; i++) {
        var e = entries[i];
        if (e.charAt(0) !== "." || dotOk) {
            var m;
            if (negate && !prefix) {
                m = !e.match(pn);
            } else {
                m = e.match(pn);
            }
            if (m) matchedEntries.push(e);
        }
    }

    var len = matchedEntries.length;
    // If there are no matched entries, then nothing matches.
    if (len === 0) return;

    // if this is the last remaining pattern bit, then no need for
    // an additional stat *unless* the user has specified mark or
    // stat explicitly.  We know they exist, since readdir returned
    // them.

    if (remain.length === 1 && !this.mark && !this.stat) {
        if (!this.matches[index]) this.matches[index] = Object.create(null);

        for (var i = 0; i < len; i++) {
            var e = matchedEntries[i];
            if (prefix) {
                if (prefix.slice(-1) !== "/") e = prefix + "/" + e;
                else e = prefix + e;
            }

            if (e.charAt(0) === "/" && !this.nomount) {
                e = path.join(this.root, e);
            }
            this._emitMatch(index, e);
        }
        // This was the last one, and no stats were needed
        return;
    }

    // now test all matched entries as stand-ins for that part
    // of the pattern.
    remain.shift();
    for (var i = 0; i < len; i++) {
        var e = matchedEntries[i];
        var newPattern;
        if (prefix) newPattern = [prefix, e];
        else newPattern = [e];
        this._process(newPattern.concat(remain), index, inGlobStar);
    }
};

GlobSync.prototype._emitMatch = function (index, e) {
    if (isIgnored(this, e)) return;

    var abs = this._makeAbs(e);

    if (this.mark) e = this._mark(e);

    if (this.absolute) {
        e = abs;
    }

    if (this.matches[index][e]) return;

    if (this.nodir) {
        var c = this.cache[abs];
        if (c === "DIR" || Array.isArray(c)) return;
    }

    this.matches[index][e] = true;

    if (this.stat) this._stat(e);
};

GlobSync.prototype._readdirInGlobStar = function (abs) {
    // follow all symlinked directories forever
    // just proceed as if this is a non-globstar situation
    if (this.follow) return this._readdir(abs, false);

    var entries;
    var lstat;
    var stat;
    try {
        lstat = this.fs.lstatSync(abs);
    } catch (er) {
        if (er.code === "ENOENT") {
            // lstat failed, doesn't exist
            return null;
        }
    }

    var isSym = lstat && lstat.isSymbolicLink();
    this.symlinks[abs] = isSym;

    // If it's not a symlink or a dir, then it's definitely a regular file.
    // don't bother doing a readdir in that case.
    if (!isSym && lstat && !lstat.isDirectory()) this.cache[abs] = "FILE";
    else entries = this._readdir(abs, false);

    return entries;
};

GlobSync.prototype._readdir = function (abs, inGlobStar) {
    var entries;

    if (inGlobStar && !ownProp(this.symlinks, abs))
        return this._readdirInGlobStar(abs);

    if (ownProp(this.cache, abs)) {
        var c = this.cache[abs];
        if (!c || c === "FILE") return null;

        if (Array.isArray(c)) return c;
    }

    try {
        return this._readdirEntries(abs, this.fs.readdirSync(abs));
    } catch (er) {
        this._readdirError(abs, er);
        return null;
    }
};

GlobSync.prototype._readdirEntries = function (abs, entries) {
    // if we haven't asked to stat everything, then just
    // assume that everything in there exists, so we can avoid
    // having to stat it a second time.
    if (!this.mark && !this.stat) {
        for (var i = 0; i < entries.length; i++) {
            var e = entries[i];
            if (abs === "/") e = abs + e;
            else e = abs + "/" + e;
            this.cache[e] = true;
        }
    }

    this.cache[abs] = entries;

    // mark and cache dir-ness
    return entries;
};

GlobSync.prototype._readdirError = function (f, er) {
    // handle errors, and cache the information
    switch (er.code) {
        case "ENOTSUP": // https://github.com/isaacs/node-glob/issues/205
        case "ENOTDIR": // totally normal. means it *does* exist.
            var abs = this._makeAbs(f);
            this.cache[abs] = "FILE";
            if (abs === this.cwdAbs) {
                var error = new Error(er.code + " invalid cwd " + this.cwd);
                error.path = this.cwd;
                error.code = er.code;
                throw error;
            }
            break;

        case "ENOENT": // not terribly unusual
        case "ELOOP":
        case "ENAMETOOLONG":
        case "UNKNOWN":
            this.cache[this._makeAbs(f)] = false;
            break;

        default: // some unusual error.  Treat as failure.
            this.cache[this._makeAbs(f)] = false;
            if (this.strict) throw er;
            if (!this.silent) console.error("glob error", er);
            break;
    }
};

GlobSync.prototype._processGlobStar = function (
    prefix,
    read,
    abs,
    remain,
    index,
    inGlobStar
) {
    var entries = this._readdir(abs, inGlobStar);

    // no entries means not a dir, so it can never have matches
    // foo.txt/** doesn't match foo.txt
    if (!entries) return;

    // test without the globstar, and with every child both below
    // and replacing the globstar.
    var remainWithoutGlobStar = remain.slice(1);
    var gspref = prefix ? [prefix] : [];
    var noGlobStar = gspref.concat(remainWithoutGlobStar);

    // the noGlobStar pattern exits the inGlobStar state
    this._process(noGlobStar, index, false);

    var len = entries.length;
    var isSym = this.symlinks[abs];

    // If it's a symlink, and we're in a globstar, then stop
    if (isSym && inGlobStar) return;

    for (var i = 0; i < len; i++) {
        var e = entries[i];
        if (e.charAt(0) === "." && !this.dot) continue;

        // these two cases enter the inGlobStar state
        var instead = gspref.concat(entries[i], remainWithoutGlobStar);
        this._process(instead, index, true);

        var below = gspref.concat(entries[i], remain);
        this._process(below, index, true);
    }
};

GlobSync.prototype._processSimple = function (prefix, index) {
    // XXX review this.  Shouldn't it be doing the mounting etc
    // before doing stat?  kinda weird?
    var exists = this._stat(prefix);

    if (!this.matches[index]) this.matches[index] = Object.create(null);

    // If it doesn't exist, then just mark the lack of results
    if (!exists) return;

    if (prefix && isAbsolute(prefix) && !this.nomount) {
        var trail = /[\/\\]$/.test(prefix);
        if (prefix.charAt(0) === "/") {
            prefix = path.join(this.root, prefix);
        } else {
            prefix = path.resolve(this.root, prefix);
            if (trail) prefix += "/";
        }
    }

    if (process.platform === "win32") prefix = prefix.replace(/\\/g, "/");

    // Mark this as a match
    this._emitMatch(index, prefix);
};

// Returns either 'DIR', 'FILE', or false
GlobSync.prototype._stat = function (f) {
    var abs = this._makeAbs(f);
    var needDir = f.slice(-1) === "/";

    if (f.length > this.maxLength) return false;

    if (!this.stat && ownProp(this.cache, abs)) {
        var c = this.cache[abs];

        if (Array.isArray(c)) c = "DIR";

        // It exists, but maybe not how we need it
        if (!needDir || c === "DIR") return c;

        if (needDir && c === "FILE") return false;

        // otherwise we have to stat, because maybe c=true
        // if we know it exists, but not what it is.
    }

    var exists;
    var stat = this.statCache[abs];
    if (!stat) {
        var lstat;
        try {
            lstat = this.fs.lstatSync(abs);
        } catch (er) {
            if (er && (er.code === "ENOENT" || er.code === "ENOTDIR")) {
                this.statCache[abs] = false;
                return false;
            }
        }

        if (lstat && lstat.isSymbolicLink()) {
            try {
                stat = this.fs.statSync(abs);
            } catch (er) {
                stat = lstat;
            }
        } else {
            stat = lstat;
        }
    }

    this.statCache[abs] = stat;

    var c = true;
    if (stat) c = stat.isDirectory() ? "DIR" : "FILE";

    this.cache[abs] = this.cache[abs] || c;

    if (needDir && c === "FILE") return false;

    return c;
};

GlobSync.prototype._mark = function (p) {
    return common.mark(this, p);
};

GlobSync.prototype._makeAbs = function (f) {
    return common.makeAbs(this, f);
};

export default GlobSync;
